/* Firmware update functions, implementation */

/* Copyright (C) 2012 David Zanetti
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; version 2 of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <util/delay.h>
#include <stdlib.h>
#include <string.h>
#include "global.h"
#include "xbootapi.h"
#include "fw_update.h"
#include "serial.h"
#include "encx24j600.h"

/* current offset into the firmware image we're writing */
uint32_t offset;
/* how far into the current page are we */
uint16_t page_offset;
/* crc we should expect. this is actually passed to xboot_install_firmware for it to check */
uint16_t new_crc;
/* pointer for the update buffer */
uint8_t *buf = NULL;

/* create the buffer, and init the offset and new CRC values */
void fw_new(uint16_t crc) {
	new_crc = crc;
	offset = 0;
	page_offset = 0;
	buf = malloc(SPM_PAGESIZE+1); /* the same size as our MCU requires */
	if (!buf) {
		serial0_tx_PGM(PSTR("fw_update: buffer malloc failed\r\n"));
	}
#ifdef DEBUG_FW_UPDATE
	serial0_tx_PGM(PSTR("fw_update: init\r\n"));
#endif
	/* ensure the temp section is erased */
	if (xboot_app_temp_erase() != XB_SUCCESS) {
		serial0_tx_PGM(PSTR("fw_update: temp erase failed\r\n"));
		free(buf);
		buf = NULL;
		return;
	}
#ifdef DEBUG_FW_UPDATE
	serial0_tx_PGM(PSTR("fw_update: temp space erased\r\n"));
#endif
	return;
}

/* apply further bytes to the buffer, writing out if we have a completed page */
uint8_t fw_temp_write(uint8_t *data, uint16_t size) {
	if (!buf) {
		serial0_tx_PGM(PSTR("fw_update: BUG: called fw_temp_write without page buffer\r\n"));
		return 0;
	}

	while (size) {
		uint16_t n;
		/* copy as much as we can in this pass to the buffer */
		if (size > (SPM_PAGESIZE - page_offset)) {
			n = SPM_PAGESIZE - page_offset;
		} else {
			n = size;
		}
#ifdef DEBUG_FW_UPDATE
		serial0_tx_PGM(PSTR("c "));
		serial0_tx_dec(n);
		serial0_tx_PGM(PSTR("\r\n"));
#endif
		memcpy(buf+page_offset,data,n);
		page_offset += n;
		/* buffer *may* contain a full page, if so, use bootloader to write it */
		if (page_offset == SPM_PAGESIZE) {
#ifdef DEBUG_FW_UPDATE
			serial0_tx_PGM(PSTR("w "));
			serial0_tx_dec(offset);
			serial0_tx_cr();
#endif
			if (xboot_app_temp_write_page(offset,buf,0) != XB_SUCCESS) {
				serial0_tx_PGM(PSTR("fw_update: temp write failed\r\n"));
				return 0;
			}
			/* write succeeded, reset how far into the page we are and adjust where we are writing to */
			page_offset = 0;
			offset += SPM_PAGESIZE;
		}
		/* adjust how far through the provided data we are */
		size -= n;
	}
	/* either we wrote some pages, or at least filled a buffer up with a bit more */
	return 1;
}

/* apply further bytes to the buffer, writing out if we have a completed page, this is sourced from an outstanding network packet */
uint8_t fw_temp_write_ethernet(uint16_t size) {
	if (!buf) {
		serial0_tx_PGM(PSTR("fw_update: BUG: called fw_temp_write without page buffer\r\n"));
		return 0;
	}

	while (size) {
		uint16_t n;
		/* copy as much as we can in this pass to the buffer */
		if (size > (SPM_PAGESIZE - page_offset)) {
			n = SPM_PAGESIZE - page_offset;
		} else {
			n = size;
		}
#ifdef DEBUG_FW_UPDATE
		serial0_tx_PGM(PSTR("c "));
		serial0_tx_dec(n);
		serial0_tx_PGM(PSTR("\r\n"));
#endif
		if (encx24j600_rx(buf+page_offset,n) != n) {
			serial0_tx_PGM(PSTR("fw_update: failed to recieve sufficent bytes\r\n"));
			return 0;
		}
		page_offset += n;
		/* buffer *may* contain a full page, if so, use bootloader to write it */
		if (page_offset == SPM_PAGESIZE) {
#ifdef DEBUG_FW_UPDATE
			serial0_tx_PGM(PSTR("w "));
			serial0_tx_dec(offset);
			serial0_tx_cr();
#endif
			if (xboot_app_temp_write_page(offset,buf,0) != XB_SUCCESS) {
				serial0_tx_PGM(PSTR("fw_update: temp write failed\r\n"));
				return 0;
			}
			/* write succeeded, reset how far into the page we are and adjust where we are writing to */
			page_offset = 0;
			offset += SPM_PAGESIZE;
		}
		/* adjust how far through the provided data we are */
		size -= n;
	}
	/* either we wrote some pages, or at least filled a buffer up with a bit more */
	return 1;
}

/* install the firmware. This writes out the last of the data buffered if required */
void fw_install(void) {
	uint16_t crc;

	if (!buf) {
		serial0_tx_PGM(PSTR("fw_update: BUG: called fw_install with no page buffer\r\n"));
		return;
	}
	/* do we have an incomplete page? */
	if (page_offset) {
#ifdef DEBUG_FW_UPDATE
		serial0_tx_PGM(PSTR("w "));
		serial0_tx_dec(offset);
		serial0_tx_cr();
#endif
		/* okay, pad the page with 0xff to prevent any CRC problems, and then write the page */
		serial0_tx_PGM(PSTR("l "));
		serial0_tx_dec(offset+page_offset);
		serial0_tx_cout(' ');
		serial0_tx_dec(SPM_PAGESIZE-page_offset);
		serial0_tx_cr();
		while (page_offset < SPM_PAGESIZE) {
			buf[page_offset++] = 0xff;
		}
		//memset(buf+page_offset,0xff,SPM_PAGESIZE-page_offset);
		/* write it */
		if (xboot_app_temp_write_page(offset,buf,0) != XB_SUCCESS) {
			serial0_tx_PGM(PSTR("fw_update: failed to write last page\r\n"));
			return;
		}
	}
	/* perform a final CRC check of the written image, so we know we can reboot */
	xboot_app_temp_crc16(&crc);
	if (crc != new_crc) {
		//serial0_tx_PGM(PSTR("fw_update: CRC check failed, update aborted\r\n"));
		serial0_tx_PGM(PSTR("fw_update: failed got "));
		serial0_tx_hex(crc >> 8);
		serial0_tx_hex(crc & 0xff);
		serial0_tx_PGM(PSTR(" needed "));
		serial0_tx_hex(new_crc >> 8);
		serial0_tx_hex(new_crc & 0xff);
		serial0_tx_cr();
		fw_discard();
		return;
	}

	/* call install in bootloader. this doesn't really install it, just writes the markers to update it */
	if (xboot_install_firmware(new_crc) != XB_SUCCESS) {
		return;
	}
	serial0_tx_PGM(PSTR("fw_update: rebooting to apply update in 10 seconds\r\n"));
	_delay_ms(10000);
	xboot_reset();
	/* never reached */
	return;
}

/* discard the new FW, and free us the SPM_PAGESIZE buffer */
void fw_discard(void) {
	if (buf) {
		free(buf);
		buf = NULL;
	}
	return;
}
